package com.nulldev.util.JVM;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.nulldev.util.JVM.reflections.ClassCache;
import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.VariableAPI.util.strings.splitters.third_party.Splitter;
import com.nulldev.util.io.FastIO.FastIO;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.Version;
import com.nulldev.util.networking.misc.TLSUtil;

public class JVM {
	private static final String s = new String();
	private static final Logger log = LoggerFactory.getLogger();
	private static final String cache_lineSep = System.getProperty("line.separator");

	public static String versionString() {
		return JRE_VERSION_STRING;
	}

	public static String uncachedVersionString() {
		String vs;
		/* Prefer java.version since we get build numbers */
		if (System.getProperty("java.version", null) != null) {
			vs = System.getProperty("java.version");
		} else {
			vs = s.getClass().getPackage().getSpecificationVersion();
		}
		if (vs == null) {
			log.warn("Unknown JVM version, spoofing 1.8!");
			return "1.8";
		}
		return vs;
	}

	public static final Splitter VERSION_PAT = Splitter.onPattern("\\.");
	public static final String JRE_VERSION_STRING = uncachedVersionString();
	public static final float JRE_VERSION = uncachedVersion();

	/**
	 * Pre JDK 9: 1.0-1.9 <br>
	 * Post JDK 10: 10.XX-1X.XX
	 * 
	 * @returns float
	 * @cached true
	 */
	public static float version() {
		return JRE_VERSION;
	}

	/**
	 * Pre JDK 9: 1.0-1.9 <br>
	 * Post JDK 10: 10.XX-1X.XX
	 * 
	 * @returns float
	 * @cached false
	 */
	public static float uncachedVersion() {
		String vs = s.getClass().getPackage().getSpecificationVersion();
		if (vs == null) {
			final String[] t = VERSION_PAT.splitToArray(System.getProperty("java.version"), false);
			vs = t[0] + "." + ArrayUtils.StringArrayUtils.addFrom(t, 1, "");
		}
		return MathUtil.safeParseF(vs, 1.8f);
	}

	public static boolean isVersion(final float version) {
		return version() == version;
	}

	private static final String BLANK_STR = "";

	public static void println() {
		FastIO.get().println(BLANK_STR);
	}

	public static void println(final String string) {
		FastIO.get().println(string);
	}

	public static void traceln(final Object object) {
		println(object);
		for (final StackTraceElement ste : Thread.currentThread().getStackTrace()) {
			println("\t" + ste);
		}
	}

	public static void println(final Object object) {
		if (object == null) {
			println("null");
			return;
		} else if (object instanceof Object[]) {
			println(Arrays.toString((Object[]) object));
			return;
		} else if (object instanceof Map<?, ?>) {
			ArrayUtils.printLln((Map<?, ?>) object);
			return;
		} else if (object instanceof List<?>) {
			ArrayUtils.printLln((List<?>) object);
			return;
		} else if (object instanceof char[]) {
			ArrayUtils.println((char[]) object);
			return;
		} else if (object instanceof byte[]) {
			println(new String((byte[]) object));
			return;
		} else if (object instanceof StackTraceElement[]) {
			println(Arrays.toString(((StackTraceElement[]) object)).replace(',', '\n'));
			return;
		}
		if (FastIO.get() == null) {
			System.out.println(object);
		} else
			FastIO.get().println(object);

	}

	public static void print(final Object object) {
		if (object == null) {
			print("null");
		}
		FastIO.get().print(object);
	}

	public static String lineSeperator() {
		return cache_lineSep;
	}

	public static void exit() {
		System.exit(-1);
	}

	public static void exit(final int code) {
		System.exit(code);
	}

	/**
	 * Yes, this will wreak havoc on the machine. <br>
	 * Don't touch me. <br>
	 * <br>
	 * What this does: <br>
	 * This keeps allocating a Object[] which will use up your CPU and your RAM.
	 * <br>
	 * This will break shit.
	 */
	public static void crash_machine() {
		Object[] o = null;
		for (;;) {
			o = new Object[]
				{ o };
			// Goodbye.
		}
	}

	/**
	 * WARNING: This is experimental. <br>
	 * Although it passed all the tests. <br>
	 * Tests: <br>
	 * <ul>
	 * <li>Re-run function (<code>PASSED</code>)</li>
	 * <li>Clean up old variables (<code>PASSED</code>)</li>
	 * <li>Run even with static code check (<code>PASSED</code>)</li>
	 * </ul>
	 */
	@SuppressWarnings(
		{ "rawtypes", "unchecked" })
	public static boolean restartVM() {
		log.info("[=>   ] Forcing GC cleanup...");
		Object obj = new Object();
		final WeakReference ref = new WeakReference<Object>(obj);
		obj = null;
		while (ref.get() != null) {
			System.gc();
		}
		System.runFinalization();
		log.info("[===> ] Reading Main Class...");
		final String mc = getMainClass();
		log.info("[====>] Executing...");
		try {
			final Class reference = ClassCache.forName(mc);
			final Method method = reference.getMethod("main", String[].class);
			String[] args = new String[]
				{ "--nullUtil.restartedVM" };
			method.invoke(null, (Object) args);
			return true;
		} catch (Exception e) {
			log.error("Failed to restart VM.", e);
			return false;
		}
	}

	public static String getMainClass() {
		// Method 1: JAVA_MAIN_CLASS
		for (final Map.Entry<String, String> entry : System.getenv().entrySet()) {
			if (entry.getKey().startsWith("JAVA_MAIN_CLASS")) {
				return entry.getValue();
			}
		}
		// Method 2: getStackTrace()
		final StackTraceElement trace[] = Thread.currentThread().getStackTrace();
		if (trace.length > 0) {
			return trace[trace.length - 1].getClassName();
		}
		// Method 3: getAllStackTraces() (technically 2.5)
		final Map<Thread, StackTraceElement[]> threadSet = Thread.getAllStackTraces();
		for (final Map.Entry<Thread, StackTraceElement[]> entry : threadSet.entrySet()) {
			for (final StackTraceElement stack : entry.getValue()) {
				try {
					String stackClass = stack.getClassName();
					if (stackClass != null && stackClass.indexOf("$") > 0) {
						stackClass = stackClass.substring(0, stackClass.lastIndexOf("$"));
					}
					final Class<?> instance = ClassCache.forName(stackClass);
					final Method method = instance.getDeclaredMethod("main", new Class[]
						{ String[].class });
					if (Modifier.isStatic(method.getModifiers())) {
						return instance.getName();
					}
				} catch (Exception e) {
					log.error("getMainClass() -> Error @ Attempt #3. (Attempt=getAllStackTraces)", e);
				}
			}
		}
		throw new IllegalStateException("getMainClass() -> Cannot determine main class.");
	}

	public static void disableAccessWarnings() {
		if (version() <= 1.8f)
			return;
		try {
			final Class<?> unsafeClass = ClassCache.forName("sun.misc.Unsafe");
			final Field field = unsafeClass.getDeclaredField("theUnsafe");
			field.setAccessible(true);
			final Object unsafe = field.get(null);

			final Method putObjectVolatile = unsafeClass.getDeclaredMethod("putObjectVolatile", Object.class, long.class, Object.class);
			final Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class);

			final Class<?> loggerClass = ClassCache.forName("jdk.internal.module.IllegalAccessLogger");
			final Field loggerField = loggerClass.getDeclaredField("logger");
			final Long offset = (Long) staticFieldOffset.invoke(unsafe, loggerField);
			putObjectVolatile.invoke(unsafe, loggerClass, offset, null);
		} catch (Exception ignored) {
		}
	}

	public static void errorln(final Throwable e) {
		if (e == null)
			return;
		FastIO.get().errln(e);
	}

	public static void errorln(final Object e) {
		if (e == null)
			return;
		else if (e instanceof StackTraceElement[]) {
			for (final StackTraceElement f : ((StackTraceElement[]) e)) {
				FastIO.get().errln(f);
			}
		} else
			FastIO.get().errln(e);
	}

	public static <T> T printlnAndReturn(final T o) {
		JVM.println(o);
		return o;
	}

	public static byte printlnAndReturn(final byte o) {
		JVM.println(o);
		return o;
	}

	public static void stackTraceHere() {
		final Exception o = new Exception();
		final String st = Exceptions.toString(o);
		println(st);
	}

	public static boolean isBigEndian() {
		return ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN);
	}

	private static final String[] DATA_WIDTH_KEYS =
		{ "sun.arch.data.model", "com.ibm.vm.bitmode", "os.arch", };

	public static int getDataWidth() {
		for (final String key : DATA_WIDTH_KEYS) {
			final String property = System.getProperty(key);
			if (property == null || property.isEmpty())
				continue;
			return (property.indexOf("64") >= 0) ? 64 : 32;
		}
		return 64;
	}

	public static boolean is64Bit() {
		return getDataWidth() == 64;
	}

	public static Version getFullVersion() {
		if (JVM.version() <= 1.8f) {
			final String[] sp = Splitters.splitString(uncachedVersionString(), ".|_");
			return new Version(sp[0], sp[1], ArrayUtils.last(sp));
		} else {
			return new Version(uncachedVersionString());
		}
	}

	static {
		TLSUtil.__static_init();
	}
}
